#ifndef TRANSFORM_H
#define TRANSFORM_H

#include <iostream>
#include <cassert>
#include "Matrix.h"
#include "Vector.h"
#include "Point.h"
#include "Ray.h"

namespace MathGraphics
{
	struct Transform 
	{
		Matrix mat;
		Matrix inv;

		Transform();
		Transform(const Matrix& m);
		Transform(const Matrix& m, const Matrix& minv);

		Transform operator* (const Transform& t) const;
		Point     operator()(const Point& p) const;
		void      operator()(const Point& p1, Point* p2) const;
		Vector    operator()(const Vector& v) const;
		void      operator()(const Vector& v1, Vector* v2) const;
		Ray       operator()(const Ray& r) const;
		void      operator()(const Ray& r1, Ray* r2) const;
	};

	Transform translate(const Vector& delta);
	Transform scale(double x, double y, double z);
	Transform rotateX(double angle);
	Transform rotateY(double angle);
	Transform rotateZ(double angle);
	Transform lookAt(const Point& pos, const Point& look, const Vector& up);
	Transform orthographic(double near, double far);

	inline Transform::Transform()
	{
		mat = inv = Matrix();
	}

	inline Transform::Transform(const MathGraphics::Matrix &m)
	{
		mat = m;
		inv = mat.inverse();
	}

	inline Transform::Transform(const MathGraphics::Matrix &m, const MathGraphics::Matrix &minv)
	{
		mat = m;
		inv = minv;
	}

	inline Transform Transform::operator *(const Transform& t) const
	{
		Matrix m1 = this->mat * t.mat;
		Matrix m2 = t.inv * this->inv;

		return Transform(m1, m2);;
	}

	inline Point Transform::operator ()(const Point& p) const
	{
		double x = p.x * mat.d[0][0] + p.y * mat.d[0][1] + p.z * mat.d[0][2] + mat.d[0][3];
		double y = p.x * mat.d[1][0] + p.y * mat.d[1][1] + p.z * mat.d[1][2] + mat.d[1][3];
		double z = p.x * mat.d[2][0] + p.y * mat.d[2][1] + p.z * mat.d[2][2] + mat.d[2][3];
		double w = p.x * mat.d[3][0] + p.y * mat.d[3][1] + p.z * mat.d[3][2] + mat.d[3][3];

		assert(w != 0);

		if (w != 1.)
		{
			double invd = 1.0f / w;
			x *= invd;
			y *= invd;
			z *= invd;
		}

		return Point(x, y, z);
	}

	inline void Transform::operator()(const Point& p1, Point* p2) const
	{
		p2->x = p1.x * mat.d[0][0] + p1.y * mat.d[0][1] + p1.z * mat.d[0][2] + mat.d[0][3];
		p2->y = p1.x * mat.d[1][0] + p1.y * mat.d[1][1] + p1.z * mat.d[1][2] + mat.d[1][3];
		p2->z = p1.x * mat.d[2][0] + p1.y * mat.d[2][1] + p1.z * mat.d[2][2] + mat.d[2][3];
		double w = p1.x * mat.d[3][0] + p1.y * mat.d[3][1] + p1.z * mat.d[3][2] + mat.d[3][3];

		assert(w != 0);

		if (w != 1.)
		{
			(*p2) /= w;
		}
	}

	inline Vector Transform::operator()(const Vector& v) const
	{
		double x = v.x * mat.d[0][0] + v.y * mat.d[0][1] + v.z * mat.d[0][2];
		double y = v.x * mat.d[1][0] + v.y * mat.d[1][1] + v.z * mat.d[1][2];
		double z = v.x * mat.d[2][0] + v.y * mat.d[2][1] + v.z * mat.d[2][2];

		return Vector(x, y, z);
	}

	inline void Transform::operator ()(const Vector& v1, Vector* v2) const
	{
		v2->x = v1.x * mat.d[0][0] + v1.y * mat.d[0][1] + v1.z * mat.d[0][2];
		v2->y = v1.x * mat.d[1][0] + v1.y * mat.d[1][1] + v1.z * mat.d[1][2];
		v2->z = v1.x * mat.d[2][0] + v1.y * mat.d[2][1] + v1.z * mat.d[2][2];
	}

	inline Ray Transform::operator ()(const Ray& r) const
	{
		Ray tr = r;

		tr.o = (*this)(r.o);
		tr.v = (*this)(r.v);

		return tr;
	}

	inline void Transform::operator ()(const MathGraphics::Ray &r1, MathGraphics::Ray *r2) const
	{
		r2->o = (*this)(r1.o);
		r2->v = (*this)(r1.v);
	}
}

#endif